Go 的 gomock 工具
转载自 GoMock 快速上手教程 (部分过时的部分进行了修改)
gomock 是什么?
GoMock 是 Go 语言官方出品的一款 mock 框架
安装 GoMock
首先,我们需要安装 gomock 包 和代码生成工具 mockgen。准确来说,即使不安装 mockgen 我们依然可以使用 GoMock,但是那样的话就需要我们自己来写 mock 代码,这样做不仅麻烦而且很容易出错。
gomock 和 mockgen 均可以使用 go get
来安装,具体命令如下:
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen@v1.6.0
可以通过执行如下命令来验证 mockgen 是否已经成功安装:
mockgen
项目上要添加依赖
go get github.com/golang/mock/mockgen/model
基本用法
- GoMock 的使用通常遵循如下四个基本步骤:
- 使用 mockgen 为你想要 mock 的接口生成一个 mock。
- 在你的测试代码中,创建一个
gomock.Controller
实例并把它作为参数传递给 mock 对象的构造函数来创建一个 mock 对象。 - 调用
EXPECT()
为你的 mock 对象设置各种期望和返回值。 - 调用 mock 控制器的
Finish()
以验证 mock 的期望行为。
通过一个简单的例子来演示 GoMock 的整个使用流程,为简单起见我们只看两个文件,一个是文件 doer/doer.go
中我们希望 mock 的接口 Doer,另一个是文件 user/user.go
中使用了 Doer 接口的结构 User。
我们想要 mock 的接口只有几行代码,这个接口有一个 DoSomething 方法,该方法接受 int 和 string 类型的参数并返回一个 error。
// doer/doer.go
package doer
type Doer interface {
DoSomething(int, string) error
}
下面是我们想 mock 掉 Doer 接口进行测试的代码:
// user/user.go
package user
import "stmock/doer"
type User struct {
Doer doer.Doer
}
func (u *User) Use() error {
return u.Doer.DoSomething(123, "Hello GoMock")
}
我们当前的项目代码结构如下:
-- doer
-- doer.go
-- user
-- user.go
我们会把 Doer 接口的 mock 代码放在项目根目录下的 mocks 包中,然后把 User 的测试代码放在 user/user_test.go
中。
-- doer
-- doer.go
-- mocks
-- mock_doer.go
-- user
-- user.go
-- user_test.go
我们首先创建一个用于放置 mock 实现的目录 mocks 然后针对 doer 包执行 mockgen 命令。
mockgen -destination=mocks/mock_doer.go -package=mocks stmock/doer Doer
也可以这样写,不过这样就是以当前目录为根路径创建的
生成的代码
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockDoer is a mock of Doer interface.
type MockDoer struct {
ctrl *gomock.Controller
recorder *MockDoerMockRecorder
}
// MockDoerMockRecorder is the mock recorder for MockDoer.
type MockDoerMockRecorder struct {
mock *MockDoer
}
// NewMockDoer creates a new mock instance.
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
mock := &MockDoer{ctrl: ctrl}
mock.recorder = &MockDoerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDoer) EXPECT() *MockDoerMockRecorder {
return m.recorder
}
// DoSomething mocks base method.
func (m *MockDoer) DoSomething(arg0 int, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DoSomething", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DoSomething indicates an expected call of DoSomething.
func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
}
注意这里自动生成的 EXPECT()
方法与要 mock 的方法(在本示例代码中就是 DoSomething)是定义在同一个对象上的,所以 EXPECT()
方法采用全大写命名可能是为了避免名字冲突(比如要 mock 的接口可能有一个 Expect 方法)。
接下来在我们的测试代码中要定义一个 mock controller。mock controller 负责追踪和验证所有与它关联的 mock 对象的期望。
我们可以通过传递一个类型为 testing.T*
的值 t 给 mock controller
的构造函数来获取一个 mock controller
对象,然后使用它来构建一个 Doer 接口的 mock。我们还需要通过 defer 的方式来调用 mock controller
的 Finish 方法,关于Finish 方法后文会有更详细的介绍。
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockDoer := mocks.NewMockDoer(mockCtrl)
假定我们要断言 mockDoer 的 Do 会被调用一次,调用时传入的参数是 123 和 Hello GoMock,返回值是 nil
。
为此,在测试用例中我们可以调用 mockDoer 的 EXPECT()
来设置期望,EXPECT()
会返回一个我们称之为 mock recorder 的对象,这个对象提供了 Doer 接口的所有方法。
调用 mock recorder 的任意方法也就根据给定的参数指定了一次期望的调用。你可以在调用之后继续链式调用其他属性,比如:
- 通过
.Return(...)
指定返回值 - 通过
.Times(number)
或.MaxTimes(number)
以及.MinTimes(number)
来指定该调用的期望次数
在本例中,我们的调用就是这样的:
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)
这样我们就完成第一个 mock 调用,以下是完整的代码示例:
// user/user_test.go
package user_test
import (
"github.com/golang/mock/gomock"
"stmock/mocks"
"stmock/user"
"testing"
)
func TestUse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockDoer := mocks.NewMockDoer(mockCtrl) // 调用之前生成的那个 Mock 的方法
testUser := &user.User{Doer: mockDoer}
// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return nil from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)
testUser.Use()
}
从上的代码中我们可能不太容易看出 mock 的期望是在哪里断言的,其实这发生在通过 defer 延迟执行的 Finish()
函数里面。在 mock controller 声明的地方就延迟调用 Finish 是一种更为符合 Go 语言习惯的写法,这样可以避免之后我们忘记对 mock 的期望进行断言。
测试结果:
=== RUN TestUse
--- PASS: TestUse (0.00s)
PASS
在实际测试中如果需要构建多个 mock,那么你可以重用 mock controller,它的 Finish 方法会对与之关联的所有 mock 的期望进行断言。
我们可能还想验证 Use 方法的返回值确实是 DoSomething 返回的,我们可以写另外一个测试,创建任意一个错误然后作为 mockDoer.DoSomething
的返回值:
func TestUseReturnsErrorFromDo(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
dummyError := errors.New("dummy error")
mockDoer := mocks.NewMockDoer(mockCtrl)
testUser := &user.User{Doer: mockDoer}
// Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return dummyError from the mocked call.
mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(dummyError).Times(1)
err := testUser.Use() // 调用了 User 的 Use() 方法
if err != dummyError {
t.Fail()
}
}
结合 go:generate 使用 GoMock
在实际开发中我们可能有很多接口或者包需要进行 mock,那么针对每个包或者接口单独运行 mockgen 来生成 mock 就比较麻烦了。为了解决这个问题,mockgen 命令可以放在一个特殊的 go:generate
注释中。
在我们的例子中,我们可以在 doer.go 文件的 package 声明下面添加一行 go:generate
注释:
package doer
//go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks stmock/doer Doer
type Doer interface {
DoSomething(int, string) error
}
使用参数匹配
有些时候你可能并不太关心调用 mock 时指定的参数。使用 GoMock,参数既可以是一个确定的值又可以是一个满足某种条件的匹配,后者被称为一个 Matcher。
Matcher 用来代表一个 mock 的方法可以接受的某个范围内的参数,以下是 GoMock 中一些预定义的 matcher:
gomock.Any()
:匹配任何类型的任何值
gomock.Eq(x)
:匹配使用反射 reflect.DeepEqual
与 x 相等的值
gomock.Nil()
:匹配等于 nil
的值
gomock.Not(m)
:(这里的 m 是一个 Matcher)匹配同 m 不匹配的值
gomock.Not(x)
:(这里的 x 不是 Matcher)匹配使用反射 reflect.DeepEqual
与 x 不相等的值
联系例子来说明,就是如果我们不关心 Do 函数的第一个参数值,那么我们可以这么写:
mockDoer.EXPECT().DoSomething(gomock.Any(), "Hello GoMock")
GoMock 会自动将不是 Matcher 类型的参数转换为 Eq 这种 matcher,所以上面的代码与下面的等价:
mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq("Hello GoMock"))
另外你也可以通过实现 gomock.Matcher
接口来定义自己的 matcher,gomock.Matcher
接口的定义如下:
//gomock/matchers.go
type Matcher interface {
Matches(x interface{}) bool
String() string
}
Matches 方法是真正用来做匹配的,而 String 方法在测试失败时用来生成人类可读的输出。比如一个用来检查参数类型的自定义 matcher 可以通过如下方式来实现:
// match/oftype.go
package match
import (
"reflect"
"github.com/golang/mock/gomock"
)
type ofType struct{ t string }
func OfType(t string) gomock.Matcher {
return &ofType{t}
}
func (o *ofType) Matches(x interface{}) bool {
return reflect.TypeOf(x).String() == o.t
}
func (o *ofType) String() string {
return "is of type " + o.t
}
我们可以通过下面代码的方式来使用这个自定义的 matcher,即期望 DoSomething 被调用一次,调用的参数是 123 和任意一个 string 类型的值,返回值是 nil。
mockDoer.EXPECT().
DoSomething(123, match.OfType("string")).
Return(nil).
Times(1)
断言调用顺序
一个对象的调用顺序通常是很重要的,调用顺序不符合预期往往代表程序是有问题的。GoMock 提供了一种确保某个调用必需发生在另外一个调用之后的机制,那就是 .After
方法,以下为示例,其指定 callFirst 方法必需在 callA 或者 callB 之前被调用。
callFirst := mockDoer.EXPECT().DoSomething(1, "first this")
callA := mockDoer.EXPECT().DoSomething(2, "then this").After(callFirst)
callB := mockDoer.EXPECT().DoSomething(2, "or this").After(callFirst)
GoMock还提供了另外一个更便捷的方法来指定不同调用之间的先后顺序,那就是 gomock.InOrder
。它使用起来不如 .After
灵活,但是可以使得较长的一串调用顺序看起来更清晰。
gomock.InOrder(
mockDoer.EXPECT().DoSomething(1, "first this"),
mockDoer.EXPECT().DoSomething(2, "then this"),
mockDoer.EXPECT().DoSomething(3, "then this"),
mockDoer.EXPECT().DoSomething(4, "finally this"),
)
指定 mock 行为
mock 对象与真正的接口实现不同,它们不实现任何接口方法,只是在被调用的时候返回已经定义好的响应并记录调用行为。
然而你可能需要你的 mock 能做的更多,这时我们就可以使用 GoMock 提供的 Do 方法了。任何 mock 调用都可以通过 .Do
来绑定一个函数,这个函数在 mock 真正被调用时就会自动执行。
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
fmt.Println("Called with x =",x,"and y =", y)
})
关于调用参数的一些复杂验证逻辑可以写在 .Do
的函数中,例如 DoSomething 的第一个 int 类型的参数应该小于等于第二个 string 类型参数的长度,那么我们就可以这样来实现:
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
if x > len(y) {
t.Fail()
}
})
相同的功能我们通过自定义 matcher 是无法实现的,因为 matcher 只能针对一个参数实现相关的匹配逻辑,无法处理多个不同参数值间的关联关系。